#ifndef __MY_OCL_HELPER__
#define __MY_OCL_HELPER__

// FIXME: this wont compile without Nvidia SDK
#ifndef __APPLE__
// In my case (optimus), Linux depends on oclUtils to make nvidia gpu work
#include <oclUtils.h>
#include <CL/opencl.h>
#else
#include <OpenCL/opencl.h>
#endif

#include <string.h>

namespace Helper {

struct ExecutionContext {
    typedef enum { NVIDIA, STANDARD } HWmode;

    HWmode hw;
    cl_platform_id id;
    cl_uint devicesCount;
    cl_device_id *devices;
    cl_context context;
    cl_command_queue queue;
    cl_program program;
    cl_kernel kernel;
    cl_mem input;
    cl_mem output;
    size_t length;
    char buffer[75];

    ExecutionContext(HWmode hardware = NVIDIA):
	hw(hardware), devicesCount(0),
	devices(NULL), context(0), queue(0), program(0), kernel(0),
	input(0), output(0)
    {
        memset(buffer, 0, sizeof(buffer));
    }

    ~ExecutionContext()
    {
        delete [] devices;
        clReleaseContext(context);
        clReleaseCommandQueue(queue);
        clReleaseProgram(program);
        clReleaseKernel(kernel);
	clReleaseMemObject(input);
	clReleaseMemObject(output);
    }

    bool initialize()
    {
        bool result = false;
        cl_int error;
        int deviceType;
        char tmp[25];

        if (hw == NVIDIA) {
#ifndef __APPLE__
            error = oclGetPlatformID(&id);
            oclCheckError(error, CL_SUCCESS);
#endif
            deviceType = CL_DEVICE_TYPE_GPU;
        } else {
            id = NULL;
            deviceType = CL_DEVICE_TYPE_ALL;
        }

        error = clGetPlatformInfo(id, CL_PLATFORM_NAME, sizeof(tmp), tmp, NULL);
        if ((result = checkReturnSuccess(error))) {
            strcpy(buffer, tmp);
        }

        error = clGetPlatformInfo(id, CL_PLATFORM_VERSION, sizeof(tmp), tmp, NULL);
        if ((result = checkReturnSuccess(error))) {
            strcat(buffer, "\t");
            strcat(buffer, tmp);
        }

        error = clGetDeviceIDs(id, deviceType, 0, NULL, &devicesCount);
        if (!devicesCount) {
            return result;
        }

	devices = new cl_device_id[devicesCount];
        if (!devices)
            result = false;
        error = clGetDeviceIDs(id, deviceType, devicesCount, devices, &devicesCount);
        if (!(result = checkReturnSuccess(error))) {
            fprintf(stderr, "####### FAIL retrieve device!\n");
            return result;
        }

        context = clCreateContext(0, devicesCount, devices, NULL, NULL, &error);
        if (!(result = checkReturnSuccess(error))) {
            fprintf(stderr, "####### FAIL create context!\n");
            return result;
        }

        // TODO: handle other devices
        clGetDeviceInfo(devices[0], CL_DEVICE_NAME, sizeof(tmp), &tmp, NULL);
        strcat(buffer, "\t");
        strcat(buffer, tmp);
        fprintf(stderr, "####### Devices %d\tbuffer: %s\n", devicesCount, buffer);

        cl_bool available = false;
        error = clGetDeviceInfo(devices[0], CL_DEVICE_AVAILABLE,
                                sizeof(cl_bool), &available, NULL);
        if (!(result = checkReturnSuccess(error))) {
            fprintf(stderr, "####### FAIL cannot test availability!\n");
            return result;
        }

        // TODO: get global memory size and other stuff

        queue = clCreateCommandQueue(context, devices[0], 0, &error);
        if ((!queue) || (!(result = checkReturnSuccess(error)))){
            fprintf(stderr, "####### FAIL cannot create command queue!\n");
            return result;
        }

        return result;
    }


    bool prepareProgram(const char *source)
    {
	bool result = false;
        cl_int error;

	program = clCreateProgramWithSource(context, 1,
					    (const char **)&source, NULL, &error);
	if ((!program) || (!(result = checkReturnSuccess(error)))) {
            fprintf(stderr, "####### FAIL kernel program creation!\n");
            return result;
	}

	error = clBuildProgram(program, 0, NULL, NULL, NULL, NULL);
	if ((!(result = checkReturnSuccess(error)))) {
	    size_t len;
	    char buffer[2048];
	    // TODO: handle more devices
	    clGetProgramBuildInfo(program, devices[0], CL_PROGRAM_BUILD_LOG,
				  sizeof(buffer), buffer, &len);
            fprintf(stderr, "####### FAIL kernel program building!"
		    "\nError: %s\n", buffer);
	    return result;
	}

	return result;
    }

    bool createKernel(char *name)
    {
	bool result = false;
        cl_int error;
	// TODO: handle multiple kernels (maybe a list/pair hash?)
	kernel = clCreateKernel(program, name, &error);
	if ((!kernel) || (!(result = checkReturnSuccess(error)))) {
            fprintf(stderr, "####### FAIL kernel compute creation!"
		" Is the kernel function name right?\n");
	}

	return result;
    }

    bool prepareBuffer(void *data, size_t bytes)
    {
	bool result = false;
        cl_int error;

	input = clCreateBuffer(context,  CL_MEM_READ_ONLY, bytes, NULL, NULL);
	output = clCreateBuffer(context, CL_MEM_WRITE_ONLY, bytes, NULL, NULL);
	if (!input || !output) {
            fprintf(stderr, "####### FAIL data buffer creation\n");
	    return result;
	}

	error = clEnqueueWriteBuffer(queue, input, CL_TRUE, 0, bytes, data,
				     0, NULL, NULL);
	if ((!(result = checkReturnSuccess(error)))) {
            fprintf(stderr, "####### FAIL write buffer setup\n");
	    return result;
	}

	return result;
    }

    // TODO: for while, there is 1 input, 1 output and 1 input parameter.
    // I need to study several kernels to better understand how this API
    // will look like.
    bool setupParameters(int parameter)
    {
	bool result = false;
	cl_int error;
	error  = clSetKernelArg(kernel, 0, sizeof(cl_mem), &input);
	error |= clSetKernelArg(kernel, 1, sizeof(cl_mem), &output);
	error |= clSetKernelArg(kernel, 2, sizeof(unsigned int), &parameter);
	if ((!(result = checkReturnSuccess(error)))) {
            fprintf(stderr, "####### FAIL setup kernel parameters\n");
	    return result;
	}

	return result;
    }

    bool execute(size_t gDomainSize, size_t lDomainSize = -1)
    {
	bool result = false;
	cl_int error;

	if (lDomainSize == -1) {
	    error = clGetKernelWorkGroupInfo(kernel, devices[0],
					     CL_KERNEL_WORK_GROUP_SIZE,
					     sizeof(lDomainSize),
					     &lDomainSize, NULL);
	    if ((!(result = checkReturnSuccess(error)))) {
		fprintf(stderr, "####### FAIL retrieving domain size\n");
		return result;
	    }
	}

	error = clEnqueueNDRangeKernel(queue, kernel, 1, NULL, &gDomainSize,
				       &lDomainSize, 0, NULL, NULL);
	if ((!(result = checkReturnSuccess(error)))) {
	    fprintf(stderr, "####### FAIL enqueue kernel\n");
	    return result;
	}

	return result;
    }

    bool readBack(void *computed, size_t bytes)
    {
	bool result = true;
	// TODO: find a way to be not blocking...
	clFinish(queue);
	cl_int error;
	error = clEnqueueReadBuffer(queue, output, CL_TRUE, 0,
				    bytes, computed, 0, NULL, NULL );
	if (!checkReturnSuccess(error)) {
	    fprintf(stderr, "####### FAIL readback results\n");
	    return false;
	}

	return result;
    }

protected:
    inline bool checkReturnSuccess(cl_int error)
    {
        if (error == CL_SUCCESS)
            return true;

        return false;
    }
};

}

#endif
